package xyz.scootaloo.kami.server.service.impl

import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND
import io.netty.handler.codec.http.HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE
import io.vertx.core.Future
import io.vertx.core.file.FileProps
import io.vertx.core.http.HttpHeaders
import io.vertx.core.http.HttpMethod
import io.vertx.core.http.HttpServerRequest
import io.vertx.core.http.impl.MimeMapping
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.StaticHandler
import io.vertx.ext.web.impl.Utils
import xyz.scootaloo.kami.server.model.AccessLevel
import xyz.scootaloo.kami.server.model.AccessToken
import xyz.scootaloo.kami.server.model.AccessType
import xyz.scootaloo.kami.server.model.plus
import xyz.scootaloo.kami.server.service.*
import xyz.scootaloo.kami.server.service.DownloadService.DownloadTask
import xyz.scootaloo.kami.server.service.impl.InternalDownloadServiceImpl.ResourcesDownloader
import java.nio.charset.Charset
import java.util.regex.Pattern
import kotlin.math.min

/**
 * [ResourcesDownloader]中的大部分代码参照于[StaticHandler], 对部分实现逻辑进行了裁剪
 *
 * @author flutterdash@qq.com
 * @since 2022/3/28 23:34
 */
object InternalDownloadServiceImpl : DownloadService {

    private val fileResolver = FileResolver()
    private val cacheService = CacheService()
    private val tokenService = TokenService()
    private val ruleService = RuleSystemService()

    override fun createDownloadLink(token: AccessToken, ipAddress: String): Future<String> {
        return LinkManager.createLink(token.path, ipAddress, token.level)
    }

    override fun download(
        ctx: RoutingContext, filename: String, ipAddress: String, token: String?
    ): Future<Unit> {
        return LinkManager.checkLegality(filename, ipAddress, token).compose { at ->
            ruleService.access((at + AccessType.DOWNLOAD))
        }.onSuccess {
            ResourcesDownloader.sendFile(ctx, filename)
        }
    }

    object LinkManager {

        private const val defTokenSize = 16
        private const val defKeepAliveTime = 60 * 60 * 6L
        private const val downloadLinkPrefix = "/download/"

        fun createLink(filename: String, ipAddress: String, userLevel: Int): Future<String> {
            if (userLevel == AccessLevel.GUEST) {
                return linkSuccessFut(filename, null)
            }

            val placeholderKey = buildPlaceholderKey(filename, ipAddress, userLevel)
            return cacheService.getString(placeholderKey).compose { existsDownloadToken ->
                if (existsDownloadToken == null) {
                    val task = DownloadTask(filename, userLevel, ipAddress)
                    generateDownloadToken(task, placeholderKey).compose { token ->
                        linkSuccessFut(filename, token)
                    }
                } else {
                    linkSuccessFut(filename, existsDownloadToken)
                }
            }
        }

        fun checkLegality(filename: String, ipAddress: String, token: String?): Future<AccessToken> {
            if (token == null) {
                return accessTokenSuccessFut(filename, AccessLevel.GUEST)
            }

            val tokenKey = buildTokenKey(token)
            return cacheService.get<DownloadTask>(tokenKey).compose { task ->
                if (task != null) {
                    var pass = true
                    if (task.ipAddress != ipAddress || task.filename != filename) {
                        pass = false
                    }
                    if (pass)
                        accessTokenSuccessFut(filename, task.userLevel)
                    else
                        accessTokenSuccessFut(filename, AccessLevel.GUEST)
                } else {
                    accessTokenSuccessFut(filename, AccessLevel.GUEST)
                }
            }
        }

        fun keepLinkAlive() {
            // todo 当有客户端在下载文件时让链接持续有效
        }

        private fun accessTokenSuccessFut(filename: String, userLevel: Int): Future<AccessToken> {
            val accessToken = AccessToken(filename, userLevel)
            return Future.succeededFuture(accessToken)
        }

        private fun linkSuccessFut(filename: String, token: String?): Future<String> {
            val link = buildDownloadLink(filename, token)
            return Future.succeededFuture(link)
        }

        private fun generateDownloadToken(task: DownloadTask, placeholder: String): Future<String> {
            return cacheService.access { asr ->
                var token = tokenService.generateRandomString(defTokenSize)
                while (asr.contains(buildTokenKey(token))) {
                    token = tokenService.generateRandomString(defTokenSize)
                }
                asr.set(buildTokenKey(token), task, defKeepAliveTime)
                asr.set(placeholder, token, defKeepAliveTime)
                token
            }
        }

        private fun buildDownloadLink(filename: String, downloadToken: String?): String {
            if (downloadToken == null)
                return "$downloadLinkPrefix$filename"
            return "$downloadLinkPrefix$filename?t=$downloadToken"
        }

        private fun buildPlaceholderKey(filename: String, ipAddress: String, userLevel: Int): String {
            return "dl:tmp:$filename:$ipAddress:$userLevel"
        }

        private fun buildTokenKey(key: String): String {
            return "dl:token:$key"
        }
    }

    object ResourcesDownloader {
        fun sendFile(ctx: RoutingContext, filename: String) {
            val realFilename = fileResolver.pathNormalize(filename)
            val absoluteFilepath = fileResolver.realPathString(realFilename)
            val fs = ctx.vertx().fileSystem()

            // 检查文件是否存在
            fs.exists(absoluteFilepath) { exists ->
                if (exists.failed()) {
                    ctx.fail(exists.cause()) // 500
                    return@exists
                }

                if (!exists.result()) {
                    ctx.fail(exists.cause())
                } else {
                    fs.props(absoluteFilepath) { props ->
                        if (props.failed()) {
                            ctx.fail(props.cause()) // 500
                        } else {
                            if (props.result().isDirectory) {
                                ctx.fail(NOT_FOUND.code()) // 404
                            } else {
                                // 文件存在, 并且不是文件夹, 可以发送
                                sendRangeFile(ctx, absoluteFilepath, props.result())
                            }
                        }
                    }
                }
            }
        }

        private fun sendRangeFile(ctx: RoutingContext, absoluteFilepath: String, props: FileProps) {
            val request = ctx.request()
            val response = ctx.response()

            if (response.closed()) {
                return
            }

            val (offset, end) = try {
                parseRange(request, props)
            } catch (e: Throwable) {
                if (e is IndexOutOfBoundsException || e is NumberFormatException) {
                    response.putHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + props.size())
                    ctx.fail(REQUESTED_RANGE_NOT_SATISFIABLE.code()) // 416
                    return
                }
                ctx.fail(e) // 500
                return
            }

            val headers = response.headers()
            // 告知客户端服务器可以处理的分段格式
            headers[HttpHeaders.ACCEPT_RANGES] = "bytes"
            // 向响应体写入请求体的长度
            headers[HttpHeaders.CONTENT_LENGTH] = (end + 1 - offset).toString()
            // 写入日期信息
            headers["date"] = Utils.formatRFC1123DateTime(System.currentTimeMillis())

            // 假如请求方式为HEAD, 则终止发送流程
            if (request.method() == HttpMethod.HEAD) {
                response.end() // 200
                return
            }

            doSendRangeFile(ctx, absoluteFilepath, props, offset, end)
        }

        private val defaultContentEncoding = Charset.defaultCharset().name() // 本地默认编码

        private fun doSendRangeFile(
            ctx: RoutingContext,
            filename: String, props: FileProps,
            offset: Long, end: Long
        ) {
            val response = ctx.response()
            val headers = response.headers()

            // 写入文件的range信息
            headers[HttpHeaders.CONTENT_RANGE] = "bytes " + offset + "-" + end + "/" + props.size()
            response.statusCode = HttpResponseStatus.PARTIAL_CONTENT.code() // 206

            val contentType = MimeMapping.getMimeTypeForFilename(filename)
            if (contentType != null) {
                if (contentType.startsWith("text")) {
                    // 假如是文本文件, 则在响应中写入编码信息
                    headers[HttpHeaders.CONTENT_TYPE] = "$contentType;charset=$defaultContentEncoding"
                } else {
                    headers[HttpHeaders.CONTENT_TYPE] = contentType
                }
            }

            val readLength = end + 1 - offset
            response.sendFile(filename, offset, readLength) { done ->
                if (done.failed()) {
                    ctx.fail(done.cause()) // 500
                }
            }
        }

        private val RANGE = Pattern.compile("^bytes=(\\d+)-(\\d*)$")

        private fun parseRange(request: HttpServerRequest, props: FileProps): Pair<Long, Long> {
            val range = request.getHeader("Range")
            var offset = 0L
            var end = props.size() - 1
            if (range != null) {
                val matcher = RANGE.matcher(range)

                // 解析起始位置
                var part: String? = matcher.group(1)
                offset = part?.toLong()!!
                if (offset < 0 || offset > props.size()) {
                    throw IndexOutOfBoundsException()
                }

                // 解析结束位置
                part = matcher.group(2)
                if (part != null && part.isNotEmpty()) {
                    // todo 当文件长度为0时
                    end = min(end, part.toLong())
                    if (end < offset) {
                        throw IndexOutOfBoundsException()
                    }
                }
            }

            return offset to end
        }
    }
}